using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using limpalex.bbparser.Exceptions;
using limpalex.bbparser.Helpers;
using limpalex.bbparser.Interfaces;
using limpalex.bbparser.Tokens;
using limpalex.bbparser.Tree;

namespace limpalex.bbparser
{
    internal class Parser : ITextProcessor
    {
        private readonly ParserOptions _options;
        private readonly SmileyConfiguration _smileyConfiguration;
        private readonly TagList _tags = new TagList();

        public ParserOptions Options
        {
            get { return _options; }
        }

        public SmileyConfiguration SmileyConfiguration
        {
            get { return _smileyConfiguration; }
        }

        public TagList Tags
        {
            get { return _tags; }
        }

        public string Process(string original)
        {
            try
            {
                if (string.IsNullOrEmpty(original))
                    return string.Empty;
                var buffer = new StringBuilder();
                BuildTree(Tokenizer.Tokenize(new StringContainer(original))).GetText(buffer, this);
                return buffer.ToString();
            }
            catch ( BaseException )
            {
                throw; //base exceptions are allowed to go through
            }
            catch (Exception ex)
            {
                throw new BaseException("parser exception occured", ex); //wrapping all other types of exceptions
            }
        }

        public Parser(ParserOptions options, SmileyConfiguration smileyConfiguration, TagList tagList)
        {
            _options = options;
            _smileyConfiguration = smileyConfiguration;
            _tags = tagList;
        }

        internal Root BuildTree(IEnumerable<BaseToken> tokens)
        {
            var root = new Root();
            var stack = new Stack<Tag>(20);
            Branch currentBranch = null;

            foreach (var token in tokens)
            {
                 if ( stack.Count == 0 ) // empty
                 {
                     if ( token.Type == TokenType.Text)
                     {
                         // on root level text tokens are always allowed
                         root.AddNode(new Leaf(token.Text));
                     }
                     else
                     {
                         var tagToken = token.To<TagToken>();

                         //closing _tags at root level are useless
                         if (tagToken.IsClosing)
                         {
                             if ( !Options.IgnoreInvalidTags)
                             {
                                 root.AddNode(new Leaf(tagToken.Text));
                             }
                             continue;
                         }

                         var tagOptions = Tags[tagToken.Name];
                         if ( tagOptions == null )
                         {
                             // unknown tag
                             if ( !Options.IgnoreUnknownTags )
                                root.AddNode(new Leaf(token.Text));
                             continue;
                         }

                         tagToken.Replacement = tagOptions.GetReplacement(tagToken);

                         if ( tagOptions.ParentRequired )
                         {
                             //tag requires parent, but is on root level
                             if ( !Options.IgnoreInvalidTags )
                                 root.AddNode(new Leaf(token.Text));
                             continue;
                         }

                         //tag may be here

                         var branch = new Branch {TagToken = tagToken, Parent = null};

                         root.AddNode(branch);

                         currentBranch = branch;
                         stack.Push(tagOptions);
                     }
                 }
                 else
                 {
                     var tagOptions = stack.Peek();
                     if ( token.Type == TokenType.Text)
                     {
                         //check whether text is allowed
                         if (tagOptions.TextAllowed)
                             currentBranch.AddNode(new Leaf(token.Text));
                     }
                     else
                     {
                         var tagToken = token.To<TagToken>();

                         if (tagToken.IsClosing)
                         {
                              if ( tagOptions.Name.Equals(tagToken.Name))
                              {
                                  //closing tag fits, moving one level up
                                  currentBranch = currentBranch.Parent;
                                  stack.Pop();
                                  continue;
                              }
                              else
                              {
                                  if ( ! Options.IgnoreInvalidTags)
                                      currentBranch.AddNode(new Leaf(tagToken.Text));
                                  continue;
                              }
                         }
                         else
                         {

                             if (!tagOptions.ChildrenAllowed)
                             {
                                 //current tag does not allow children
                                 if (!Options.IgnoreInvalidTags)
                                     root.AddNode(new Leaf(token.Text));
                                 continue;
                             }
                             if (tagOptions.AllowedChildren.Count > 0)
                             {
                                 if (!tagOptions.AllowedChildren.Contains(tagToken.Name))
                                 {
                                     //allowed children do not contain this tag
                                     if (!Options.IgnoreInvalidTags)
                                         root.AddNode(new Leaf(token.Text));
                                     continue;
                                 }
                             }

                             var newTagOptions = Tags[tagToken.Name];

                             if ( newTagOptions == null)
                             {
                                  // unknown tag
                                  if (!Options.IgnoreUnknownTags)
                                      currentBranch.AddNode(new Leaf(token.Text));
                                  continue;
                             }

                             tagToken.Replacement = newTagOptions.GetReplacement(tagToken);

                             if ( newTagOptions.AllowedParents.Count > 0)
                             {
                                 if (!newTagOptions.AllowedParents.Contains(tagOptions.Name))
                                 {
                                     //cannot be nested within this parent
                                     if (!Options.IgnoreInvalidTags)
                                         currentBranch.AddNode(new Leaf(token.Text));
                                     continue;
                                 }
                             }

                             if ( newTagOptions.ParametersRequired.Count > 0)
                             {
                                 foreach ( var parameter in newTagOptions.ParametersRequired)
                                 {
                                     var s = parameter;
                                     if (Array.Exists(tagToken.Attributes.AllKeys, key => key == s)) continue; 
                                                         //doesn't contain one of the required attributes
                                        
                                     if (!Options.IgnoreInvalidTags)
                                         currentBranch.AddNode(new Leaf(token.Text));
                                     continue;
                                 }
                             }

                             // tag can be here

                             var branch = new Branch {Parent = currentBranch, TagToken = tagToken, Tag = tagOptions};

                             currentBranch.AddNode(branch);
                             currentBranch = branch;

                             stack.Push(tagOptions);
                         }
                     }
                 }
            }

            // checking whether required text and required childs exist
            CheckTree(root);

            return root;
        }

        private void CheckTree(INode node)
        {
            switch (node.Type)
            {
                case NodeType.Root:
                    foreach (var n in ((Root)node))
                    {
                        CheckTree(n);                        
                    }
                    break;
                case NodeType.Branch:

                    var current = (Branch)node;
                    var tag = Tags[current.TagToken.Name];

                    if ( tag.ChildrenRequired)
                    {
                        if ( current.NodesCount == 0)
                        {
                            //invalid tag, should be removed
                            //TODO: maybe replace by raw text if some options are present...
                            current.Parent.RemoveNode(current);

                            return; // node is removed, so no other checks are required
                        }
                    }

                    if ( tag.TextRequired)
                    {
                        if ( !Tokenizer.HasAnyTextNodes(current) )
                        {
                            //invalid tag, should be removed
                            //TODO: maybe replace by raw text if some options are present...
                            current.Parent.RemoveNode(current);
                            return; // node is removed, so no other checks are required
                        }
                    }

                    foreach (var n in (current))
                    {
                        CheckTree(n);
                    }
                    break;
                case NodeType.Leaf:
                    return;
                default:
                    throw new ArgumentOutOfRangeException("node");
            }
            
        }
    }
}